/*
 * Copyright (c) 2020-2099 the original author or authors. All rights reserve.
 */
package org.tianyun.cloud.rest.utils;

import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.tianyun.cloud.common.constants.ResponseCode;
import org.tianyun.cloud.exception.BusinessException;
import org.tianyun.cloud.utils.JsonUtils;
import org.tianyun.cloud.utils.StreamUtils;
import org.tianyun.cloud.utils.XmlUtils;

import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;

/**
 * RestTemplate的封装工具类，如需做文件上传，body参数必须是MultiValueMap，mediaType需设置为MediaTypeEnum.MULTIPART_FORM_DATA
 *
 * <p>
 * 示例代码：<code>
 * Optional<String> result = RestBuilder.of(restTemplate)
 * .thrownException() // or setFailedCode("xxx")
 * .snakeCase() // or lowerCamelCase()
 * .charset("utf-8")
 * .mediaType(MediaTypeEnum.APPLICATION_XML)
 * .domain("http://www.baidu.com")
 * .postForObject("/s", String.class);
 * </code>
 *
 * @auther ebert_chan
 */
public class RestBuilder {

    private static final Logger LOGGER = LoggerFactory.getLogger(RestBuilder.class);

    private static final String TARGET_EXCEPTION = "_target_";

    /**
     * 通过RestTemplate创建RestBuilder
     *
     * @param restTemplate
     * @return
     * @auther ebert_chan
     */
    public static RestBuilder of(RestTemplate restTemplate) {

        Assert.notNull(restTemplate, "restTemplate is not null");

        RestBuilder builder = new RestBuilder();
        builder.restTemplate = restTemplate;
        return builder;
    }

    /**
     * RestTemplate
     */
    private RestTemplate restTemplate;

    /**
     * MediaType，默认APPLICATION_JSON
     */
    private MediaTypeEnum mediaType = MediaTypeEnum.APPLICATION_JSON;

    /**
     * 序列化策略，仅对JSON或XML生效
     */
    private PropertyNamingStrategy strategy = PropertyNamingStrategy.LOWER_CAMEL_CASE;

    /**
     * 当code不为空时，调用失败将抛出BusinessException，code业务错误码； 未设置时仅记录错误日志， 返回结果为null
     */
    private String code;

    /**
     * BusinessException异常业务参数
     */
    private Object[] args;

    /**
     * 域名
     */
    private String domain;

    /**
     * 编码格式，默认utf-8
     */
    private String charset = "utf-8";

    /**
     * 构造方法
     */
    private RestBuilder() {
    }

    /**
     * 下划线命名策略，仅对JSON或XML生效
     *
     * @return
     * @auther ebert_chan
     */
    public RestBuilder snakeCase() {
        return ofStrategy(PropertyNamingStrategy.SNAKE_CASE);
    }

    /**
     * 驼峰命名策略，仅对JSON或XML生效
     *
     * @return
     * @auther ebert_chan
     */
    public RestBuilder lowerCamelCase() {
        return ofStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE);
    }

    /**
     * 设置序列化策略，仅对JSON或XML生效
     *
     * @param strategy
     * @return
     * @auther ebert_chan
     */
    public RestBuilder ofStrategy(PropertyNamingStrategy strategy) {
        Assert.notNull(strategy, "strategy is not null");
        RestBuilder builder = new RestBuilder();
        builder.strategy = strategy;
        return builder;
    }

    /**
     * 设置编码格式
     *
     * @param charset
     * @return
     * @auther ebert_chan
     */
    public RestBuilder charset(String charset) {
        Assert.notNull(charset, "charset is not null");
        this.charset = charset;
        return this;
    }

    /**
     * 设置mediaType
     *
     * @param mediaType
     * @return
     * @auther ebert_chan
     */
    public RestBuilder mediaType(MediaTypeEnum mediaType) {
        Assert.notNull(mediaType, "mediaType is not null");
        this.mediaType = mediaType;
        return this;
    }

    /**
     * 设置domain
     *
     * @param mediaType
     * @return
     * @auther ebert_chan
     */
    public RestBuilder domain(String domain) {
        Assert.notNull(domain, "domain is not null");
        this.domain = domain;
        return this;
    }

    /**
     * 设置调用失败（响应码不为ResponseCode.SUCCESS）的业务异常码，抛出异常BusinessException
     *
     * @param code
     * @return
     * @auther ebert_chan
     */
    public RestBuilder setFailedCode(String code, Object... args) {
        this.code = code;
        this.args = args;
        return this;
    }

    /**
     * 如果调用失败，抛出BusinessException异常
     *
     * @return
     * @auther ebert_chan
     */
    public RestBuilder thrownException() {
        this.code = TARGET_EXCEPTION;
        return this;
    }

    /**
     * http get请求
     * <p>
     *
     * @param url         请求接口地址
     * @param queryParams 请求参数(URL后面)
     * @param returnType  返回结果类型
     * @return
     * @author ebert_chan
     */
    public <V, T> Optional<V> getForObject(String url, Map<String, Object> queryParams, Class<? extends V> returnType) {
        ResponseEntity<byte[]> response = null;
        try {
            url = StreamUtils.url(domain, url);
            LOGGER.info("request [{}]", url);

            if (CollectionUtils.isEmpty(queryParams)) {
                response = restTemplate.getForEntity(url, byte[].class);
            } else {
                url = this.appendUriVariables(url, queryParams);
                response = restTemplate.getForEntity(url, byte[].class, queryParams);
            }

        } catch (Throwable e) {
            this.handlerException(e);
        }
        return this.handlerResponse(response, returnType);
    }

    /**
     * http get请求
     * <p>
     *
     * @param url        请求接口地址
     * @param returnType 返回结果类型
     * @return
     * @author ebert_chan
     */
    public <V, T> Optional<V> getForObject(String url, Class<? extends V> returnType) {
        return this.getForObject(url, null, returnType);
    }

    /**
     * http post请求
     * <p>
     *
     * @param url         请求接口地址
     * @param queryParams 请求参数(URL后面)
     * @param request     请求参数(Body)
     * @param returnType  返回结果的类型
     * @return
     * @author ebert_chan
     */
    public <V, T> Optional<V> postForObject(String url, Map<String, Object> queryParams, T request, Class<? extends V> returnType) {
        url = StreamUtils.url(domain, url);
        LOGGER.info("request [{}]", url);

        HttpEntity<?> httpEntity = null;

        if (request != null) {
            if (request instanceof HttpEntity) {
                httpEntity = (HttpEntity<?>) request;
            } else if (request instanceof MultiValueMap) {
                HttpHeaders headers = new HttpHeaders();
                switch (mediaType) {
                    case MULTIPART_FORM_DATA:
                        headers.setContentType(MediaType.MULTIPART_FORM_DATA);
                        break;
                    default:
                        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
                        break;
                }
                httpEntity = new HttpEntity<>(request, headers);
            }
        }
        if (httpEntity == null) {
            String body = this.serialize(request);
            LOGGER.info("request body: {}", body);
            HttpHeaders headers = this.getHttpHeaders();
            httpEntity = new HttpEntity<String>(body, headers);
        }

        ResponseEntity<byte[]> response = null;
        try {
            if (CollectionUtils.isEmpty(queryParams)) {
                response = restTemplate.postForEntity(url, httpEntity, byte[].class);
            } else {
                url = this.appendUriVariables(url, queryParams);
                response = restTemplate.postForEntity(url, httpEntity, byte[].class, queryParams);
            }
        } catch (Throwable e) {
            this.handlerException(e);
        }
        return this.handlerResponse(response, returnType);
    }

    /**
     * http post请求
     * <p>
     *
     * @param url        请求接口地址
     * @param request    请求参数(Body)
     * @param returnType 返回结果的类型
     * @return
     * @author ebert_chan
     */
    public <V, T> Optional<V> postForObject(String url, T request, Class<? extends V> returnType) {
        return this.postForObject(url, null, request, returnType);
    }

    /**
     * http post请求
     * <p>
     *
     * @param url        请求接口地址
     * @param returnType 返回结果的类型
     * @return
     * @author ebert_chan
     */
    public <V, T> Optional<V> postForObject(String url, Class<? extends V> returnType) {
        return this.postForObject(url, null, null, returnType);
    }

    private <T> HttpHeaders getHttpHeaders() {
        HttpHeaders headers = new HttpHeaders();
        switch (mediaType) {
            case APPLICATION_XML:
                headers.setContentType(MediaType.APPLICATION_XML);
                break;
            default:
                headers.setContentType(MediaType.APPLICATION_JSON);
                break;
        }
        return headers;
    }

    private <T> String serialize(T request) {
        String body = null;
        switch (mediaType) {
            case APPLICATION_XML:
                body = request != null ? XmlUtils.of(strategy).toXmlString(request) : null;
                break;
            default:
                body = request != null ? JsonUtils.of(strategy).toJsonString(request) : "{}";
                break;
        }
        return body;
    }

    /**
     * 处理响应
     * <p>
     *
     * @param response
     * @param returnType
     * @return
     * @author ebert_chan
     */
    @SuppressWarnings({"unchecked"})
    private <V> Optional<V> handlerResponse(ResponseEntity<byte[]> response, Class<? extends V> returnType) {
        if (response == null) {
            return Optional.empty();
        }

        byte[] body = response.getBody();
        if (body == null) {
            LOGGER.info("response data is null");
            return Optional.empty();
        }

        if (returnType.equals(byte[].class)) {
            LOGGER.info("response data: byte[{}]", body.length);
            return Optional.ofNullable((V) body);
        }

        MediaType mediaType = response.getHeaders().getContentType();
        V result = null;
        try {
            if (MediaType.APPLICATION_JSON.compareTo(mediaType) == 0 || mediaType.toString().indexOf("json") != -1) {
                result = this.parse(body, returnType, (data, type) -> JsonUtils.of(this.strategy).parse(data, type));
            } else if (MediaType.APPLICATION_XML.compareTo(mediaType) == 0 || mediaType.toString().indexOf("xml") != -1) {
                result = this.parse(body, returnType, (data, type) -> XmlUtils.of(this.strategy).parse(data, type));
            } else if (MediaType.TEXT_HTML.compareTo(mediaType) == 0 || mediaType.toString().indexOf("html") != -1) {
                String data = new String(body, charset);
                if (returnType.equals(String.class)) {
                    return Optional.ofNullable((V) data);
                }
                LOGGER.warn("response context-type is text/html, returnType must be String or byte[].");
            } else {
                LOGGER.warn("response data: byte[{}], returnType not matcher.", body.length);
            }
        } catch (UnsupportedEncodingException e) {
            LOGGER.error("UnsupportedEncodingException", e);
        }
        return Optional.ofNullable(result);
    }

    @SuppressWarnings("unchecked")
    private <V> V parse(byte[] body, Class<? extends V> returnType, BiFunction<String, Class<? extends V>, V> deserialization) throws UnsupportedEncodingException {
        V result = null;
        String data = new String(body, charset);
        LOGGER.info("response data: {}", data);
        if (returnType.equals(String.class)) {
            return (V) data;
        }
        result = deserialization.apply(data, returnType);
        if (result == null && this.code != null) {
            throw new BusinessException(ResponseCode.BUSY_EXCEPTION);
        }
        return result;
    }

    /**
     * 处理异常
     *
     * @param e
     * @auther ebert_chan
     */
    private <V> void handlerException(Throwable e) {
        if (this.code != null) {
            if (this.code == TARGET_EXCEPTION) {
                throw new BusinessException(ResponseCode.BUSY_EXCEPTION, e);
            }
            throw new BusinessException(this.code, e, this.args);
        } else {
            LOGGER.warn("http request exception", e);
        }
    }

    /**
     * 添加queryParams
     * <p>
     *
     * @param url
     * @param uriVariables
     * @return
     * @author ebert_chan
     */
    private String appendUriVariables(String url, Map<String, ?> uriVariables) {
        StringBuilder sb = new StringBuilder();
        int index = url.indexOf("?");
        if (index != -1) {
            sb.append(url.substring(0, index));
        } else {
            sb.append(url);
        }
        sb.append("?");
        uriVariables.keySet().forEach(key -> {
            sb.append(key).append("=").append("{").append(key).append("}").append("&");
        });
        sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }

    public static enum MediaTypeEnum {

        APPLICATION_JSON, APPLICATION_XML, APPLICATION_FORM_URLENCODED, MULTIPART_FORM_DATA;

    }

}
